gqlgen でGraphQLサーバーを半年間運用してみた感想
まとめ
STELLAではGraphQLサーバーを運用している
Goでgqlgenを使っている
スキーマファースト
schemaからコードを生成してくれる
生成されたコードも型安全
dataloaden で n+1 対応が楽だった
スキーマの設計
query/mutation 名
input
relay-style paginationの実装が標準では用意されていない
そのうち入るかも?
アクセス制御
directive
gqlgenは1.0に向けて活発に開発されているので、期待している
contributeする機会があればやりたい
---
以下記事
これは GraphQL Advent Calendar 2019 1日目の記事です
こんにちは、ebiken です。
自分は今スタートアップでバックエンドエンジニアとして、gqlgenを使ったGraphQLサーバーを運用しています。運用し始めて半年ほど経ったので感想とかを色々書いていきます。
具体的な技術スタックは、APIサーバー Go gqlgen、iOSアプリSwift apollo-ios、管理画面(Vue.js) vue-apollo です。
まずは軽くGraphQLとgqlgenについて紹介します。
GraphQL
graphql.org
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
GraphQLはAPIのためのクエリ言語/実装です。もともとFacebookが開発してOSS化されたもので、現在は GraphQL Foundation が推進しています。
GraphQLを推進する「GraphQL Foundation」設立、Facebookの手を離れLinux Foundation傘下へ
仕様や実装などについて書かれているおすすめの記事を載せておきます。
- 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ
gqlgen
gqlgen.com
GitHub
gqlgen is a Go library for building GraphQL servers without any fuss.
gqlgenはスキーマファースト、型安全、コード生成という特徴を持ったGoのGraphQLサーバーライブラリです。
gqlgen - getting started
---
GraphQL, gqlgenの特徴についてそれぞれ運用してきた感想を書いていきます。
スキーマファースト
gqlgenはGraphQLのスキーマからコードを生成し、作成されたresolverのinterfaceを満たすように実装するというフローになります。そのようにschemaを最初に設計し、それをもとにサーバー/クライアントの実装を進めることをスキーマファースト開発といいます。
開発チームの背景としてリモート開発がメイン、サーバー/クライアントを実装する人が別というのがあったため、スキーマファーストで開発を進めることは非常に重要でした。
具体的なフローはこんな感じです。
1. スキーマを書く
code:graphql
query {
...
users: UserConnection!
}
2. 新しいスキーマで go generate を行い、interfaceを満たす最低限の resolver を書く(具体的な実装はしない)
code:go
func (r *queryResolver) Users(ctx context.Context) (*UserConnection, error) {
panic("not implemented")
}
3. introspection - apollo-tooling を利用してフロントエンドで使用する型を定義したファイルを作成する
code:sh
$ apollo schema:download <schema.json ファイルのパス> \
--endpoint <GraphQLサーバーのエンドポイント> \
--header <認証用のヘッダー>
apollo-tooling ドキュメント
4. サーバー/フロントの実装を進める
このようなフローになっているので、実装後にサーバー/クライアント間の擦り合せ(パラメータが違う、型が違うなど)をする必要がなくなりコミュニケーションコストを減らすことができています。
スキーマファースト開発について参考にした記事を貼っておきます。
マイクロサービスにおけるWeb APIスキーマの管理 ─ GraphQL、gRPC、OpenAPIの特徴と使いどころ
n+1問題
GraphQLを使う際に発生する問題としてn+1問題が挙げられます。GraphQLはresolverを別々に実行していくため、同じリソースに対しても都度DB等へのリクエストを行ってしまうのは良くないです。
n+1問題に対してgqlgenは dataloaden を使用して実装することを推奨しているのですが、こちらも go generate ベースで型安全なバッチ処理を簡単に実装する事ができるので凄く良いです。
dataloaden
pagination
GraphQLサーバーでページネーションをする場合、推奨されているのが relay style pagination です。仕様はFacebookが出しているものがあるのでそれを満たせば良いです。(Relay Cursor Connections Specification )
しかし仕様に記述されているアルゴリズムはリストを全部取ってきて処理するという形式のため、パフォーマンス等を考慮するとRDBを使用している場合そのまま実装するわけにはいきません。そのため、first/last/before/after の指定に合わせてクエリを組み立てるよう自前で実装しました。
実装にはこちらで議論されているものを参考にしました。
graphql/graphql-relay-js Pagination implementation for real-life database #94
このpaginationの実装に関してはgqlgenにpluginとして入れられるようになりそうな話が出ていました。
99designs/gqlgen #752
スキーマの設計
GraphQLの公式ページにはスキーマの書き方や機能について書いてあるものの、それを実際にどう使っていくのかといった情報はまだ少ないです。そのため最初はスキーマの設計に苦労しました。何度かスキーマのリファクタリングをしています。
実際に使用しているスキーマの設計はこんな感じです。
ID はglobalでユニークな値を指定する
引数には XXInput という名前で input オブジェクトを使用する
code:graphql
# schema.graphql
mutation {
updateUser(input: UpdateUserInput!): User!
}
# user.graphql
input UpdateUserInput {
name: String!
}
オブジェクトのリストを返したい場合はtypeを Connection として定義する
pagination用のInputは PaginationInput として定義し引数に使用する
code:graphql
# schema.graphql
query {
users(page: PaginationInput!): UserConnection!
}
# page.graphql
input PaginationInput {
first: Int
last: Int
before: String
after: String
}
Connection, Edge, Node interfaceを定義しpaginationものはそれを継承する
code:graphql
# user.graphql
type User implements Node {
identifier: ID!
name: String!
}
type UserEdge implements Edge {
cursor: String!
node: User!
}
type UserConnection implements Connection {
pageInfo: PageInfo!
edges: UserEdge!
}
# page.graphql
type PageInfo {
startCursor: String!
endCursor: String!
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
interface Connection {
pageInfo: PageInfo!
edges: Edge!
}
interface Edge {
cursor: String!
node: Node!
}
interface Node {
identifier: ID!
}
orderが複数あるクエリの場合、 orderBy inputを指定する
code:graphql
# schema.graphql
query {
posts(page: PaginationInput!, orderBy: PostOrder!): PostConnection!
}
# post.graphql
input PostOrder {
field: PostOrderFields!
}
enum PostOrderFields {
CREATED_AT
POPULARITY
}
このあたりを参考にしています。特に9月の技術書展7でvvakameさんが執筆されたGraphQLスキーマ設計ガイドは非常に参考になりました。
『GraphQLスキーマ設計ガイド』を技術書典7で頒布します
GitHub GraphQL API v4
GraphQL best practices for GraphQL schema design
アクセス制御
管理画面からのみ一部のquery/mutationに対してアクセスを許可したいという要件があったため、directive を使用しました。
一部のRoleからのみのアクセスを許可するのを以下のようにして実現しています。
code:graphql
# directive の定義
directive @hasRole(role: Role!) on FIELD_DEFINITION
enum Role {
ADMIN
}
# directiveの使用
mutation {
updateUserByAdmin(input: UpdateUserByAdminInput!): User! @hasRole(role: ADMIN)
}
Directives
gqlgen - Schema Directives
上記に書いた以外にもGraphQLを使った開発には多くのメリットがあり、特にDX(Developer Experience)が良いと思っています。
GraphiQL がドキュメントになり簡単に動作確認できますし、graphql-faker などを使用すれば簡単にmockサーバーを立ち上げることができます。
そのような高いDXがあり、速い開発スピードも出せているので、GraphQL+gqlgenを選んで良かったと思っています。
今から作るなら Amplify あたりも良さそうと思っています。